var Ydragdrop = {
	BIG_Z_INDEX: 10000,
	THRESHOLD: 7,
	SORTABLE_UL_SELECTOR: 'ul.sortable',
	holding: null,
	draging: null,
	selected: null,
	containers: null,
	
	enhanceDragable: function(e, dragTarget) {
		if (e.onmousedown == Ydragdrop.onMouseDown) return;
		e.oMouseDown = e.onmousedown;
		e.onmousedown = Ydragdrop.onMouseDown;
		
		if (Yutil.isIE) {
			// disable annoying text selection in IE
			e.ondrag = function() {return false;}
			e.onselectstart = function() {return false;}
		}
		
		if (dragTarget) e.dragTarget = dragTarget;
	},
	
	onMouseDown: function(event, selected) {
		// called by e.onmousedown, that is why 'this' works
		var e = this.dragTarget || this;
		var s = selected || [e];
		event = event || window.event;
		if (e.oMouseDown) e.oMouseDown(event);
		
		// remember element's initial status
		for (var i = 0, f; i < s.length; i++) {
			f = s[i];
			f.oOffset = [event.clientX, event.clientY];
			f.dOffset = [event.clientX, event.clientY];
			f.oOpacity = f.style.opacity;
			f.oZIndex = f.style.zIndex;
		}

		Ydragdrop.holding = e;
		Ydragdrop.selected = s;
		Ydragdrop.winSize = Yutil.winSize();
		document.onmousemove = Ydragdrop.onMouseMove;
		document.onmouseup = Ydragdrop.onMouseUp;
		
		return false; // cancel selection behavior
	},
	
	onMouseMove: function(event) {
		var e = Ydragdrop.holding;
		var s = Ydragdrop.selected;
		event = event || window.event;
		var x = event.clientX, y = event.clientY;

		if (x < 0 || y < 0 || x >= Ydragdrop.winSize[0] || y >= Ydragdrop.winSize[1])
			return false;

		// drag started ?
		if (!Ydragdrop.draging) {
			if (Yutil.distance(x, y, e.oOffset[0], e.oOffset[1]) < Ydragdrop.THRESHOLD)
				return;
			// drag start
			Ydragdrop.draging = e;
			for (var i = 0, f; i < s.length; i++) {
				f = s[i];
				f.style.zIndex = Ydragdrop.BIG_Z_INDEX;
				f.style.opacity = 0.75;
			}
			Ydragdrop._notifyDragStart(e, s, event);
		}
		
		// move e to the new position
		var p = Yutil.pos(e);
		var nx = p[0] + x - e.dOffset[0], ny = p[1] + y - e.dOffset[1];
		// gives a chance to adjust the new position
		var np = Ydragdrop._notifyBeforeDragMove(e, [nx, ny], event);
		if (np)
			Yutil.moveTo(e, np[0], np[1]);
		e.dOffset = [x + np[0] - nx, y + np[1] - ny];

		// notify drag move, and ENSURE pos before and after is the same
		var absPosBefore = Yutil.abspos(e);
		Ydragdrop._notifyDragMove(e, s, np, event);
		var absPosAfter = Yutil.abspos(e);
		var dx = absPosBefore[0] - absPosAfter[0];
		var dy = absPosBefore[1] - absPosAfter[1];
		if (dx != 0 || dy != 0) {
			p = Yutil.pos(e);
			np = [p[0] + dx, p[1] + dy];
			Yutil.moveTo(e, np[0], np[1]);
		}
		
		// all selected follow e's movement
		for (var i = 0, f; i < s.length; i++) {
			f = s[i];
			if (f == e) continue;
			// assumes 'position:relative'
			Yutil.moveTo(f, np[0], np[1]);
		}
		
		return false; // cancel selection behavior
	},
	
	onMouseUp: function(event) {
		event = event || window.event;
		var e = Ydragdrop.draging;
		var s = Ydragdrop.selected;

		document.onmousemove = null;
		document.onmouseup = null;
		Ydragdrop.holding = null;
		Ydragdrop.draging = null;
		Ydragdrop.selected = null;
		
		if (e) {
			for (var i = 0, f; i < s.length; i++) {
				f = s[i];
				f.style.opacity = f.oOpacity;
				f.style.zIndex = f.oZIndex;
			}
			// cancel the click event caused by drag & drop
			e.mouseUpTime = new Date().getTime();
			if (e.onclick != Ydragdrop._cancelOneClick) {
				e.oOnClick = e.onclick;
				e.onclick = Ydragdrop._cancelOneClick;
			}
			Ydragdrop._notifyDragEnd(e, s, event);
		}
		return false; // cancel selection behavior
	},
	
	_cancelOneClick: function(event) {
		this.onclick = this.oOnClick;
		this.oOnClick = null;
		if (new Date().getTime() - this.mouseUpTime < 800) {
			// cancel the click triggered by drag & drop
			Event.stop(event || window.event);
			return false;
		} else {
			if (this.onclick)
				this.onclick(event);
		}
	},
	
	_notifyDragStart: function(e, selected, event) {
		if (e.onDragStart) e.onDragStart(event);
		var p = e.parentNode;
		if (p.onChildDragStart) p.onChildDragStart(e, selected, event);
	},
	
	_notifyBeforeDragMove: function(e, newPos, event) {
		if (e.beforeDragMove)
			return e.beforeDragMove(newPos, event);
		var p = e.parentNode;
		if (p.beforeChildDragMove)
			return p.beforeChildDragMove(e, newPos, event);
		return newPos;
	},
	
	_notifyDragMove: function(e, selected, newPos, event) {
		if (e.onDragMove) e.onDragMove(newPos, event);
		var p = e.parentNode;
		if (p.onChildDragMove) p.onChildDragMove(e, selected, newPos, event);
	},
	
	_notifyDragEnd: function(e, selected, event) {
		if (e.onDragEnd) e.onDragEnd(event);
		var p = e.parentNode;
		if (p.onChildDragEnd) p.onChildDragEnd(e, selected, event);
	},
	
	enhanceSortableUl: function(ul) {
		ul.onChildDragStart = Ydragdrop.liOnDragStart;
		ul.beforeChildDragMove = Ydragdrop.liBeforeDragMove;
		ul.onChildDragMove = Ydragdrop.liOnDragMove;
		ul.onChildDragEnd = Ydragdrop.liOnDragEnd;
		ul.onmousedown = Ydragdrop.ulOnMouseDown;
		if (Yutil.isIE) {
			// disable annoying text selection in IE
			ul.ondrag = function() {return false;}
			ul.onselectstart = function() {return false;}
		}
	},
	
	ulOnMouseDown: function(event) {
		event = event || window.event;
		var src = event.target || event.srcElement;
		var li = 'li'.enclosing(src, this);
		if (li == null) return;
		
		var selected = null;
		if ('selected'.hasClass(li) && this.selected)
			selected = 'li.selected'.resolve(this);
		Ydragdrop.onMouseDown.apply(li, [event, selected]);
		return false; // cancel selection behavior
	},
	
	liOnDragStart: function(li, selected, event) {
		// collect all possible containers of the dragging <li>
		Ydragdrop.containers = [];
		var liCls = Yutil.classes(li);
		for (var i = 0, f; i < selected.length; i++) {
			var f = selected[i];
			liCls = liCls.retain(Yutil.classes(f))
		}
		liCls.push('*');
		var uls = jQuery.find(Ydragdrop.SORTABLE_UL_SELECTOR), ulAccept;
		for (var i = 0; i < uls.length; i++) {
			ulAccept = (uls[i].getAttribute('accept') || '').splitExcludeBlank();
			if (ulAccept.retain(liCls))
				Ydragdrop.containers.push(uls[i]);
		}
		
		// make sure all selected are in one big block
		Ydragdrop._insertBefore(li.parentNode, selected, li.nextSibling);
	},
	
	liOnDragMove: function(li, selected, newPos, event) {
		// is <li> going into another container?
		if (Ydragdrop.containers) {
			var c, f = selected.first(), l = selected.last();
			var fp = Yutil.abspos(f), lp = Yutil.abspos(l);
			for (var i = 0, c; i < Ydragdrop.containers.length; i++) {
				c = Ydragdrop.containers[i];
				if (Yutil.intersects(fp, f.offsetWidth, lp[1]-fp[1]+l.offsetHeight, Yutil.abspos(c), c.offsetWidth, c.offsetHeight))
					break;
				c = null;
			}
			
			if (c != null && c != li.parentNode) {
				// move <li> to the other container
				var oldc = li.parentNode;
				Ydragdrop._insertBefore(c, selected, null);
				if ('selected'.hasClass(li)) {
					c.selected = 'li.selected'.resolve(c);
					oldc.selected = 'li.selected'.resolve(oldc);
				}
			}
		}
		
		// swap <li> into the right position
		var e = selected.last()
		var a = e;
		var b = 'li'.nextOf(e);
		while (b != null && e.offsetTop >= b.offsetTop - 4) {
			a = b;
			b = 'li'.nextOf(b);
		}
		if (a != selected.last()) {
			Ydragdrop._insertBefore(li.parentNode, selected, b);
			return;
		}
		
		e = selected.first();
		a = 'li'.previousOf(e);
		b = e;
		while (a != null && e.offsetTop <= a.offsetTop + 4) {
			b = a;
			a = 'li'.previousOf(a);
		}
		if (b != e) {
			Ydragdrop._insertBefore(li.parentNode, selected, b);
			return;
		}
	},
	
	liOnDragEnd: function(li, selected, event) {
		// reset <li> posistion
		for (var i = 0, f; i < selected.length; i++) {
			var f = selected[i];
			Yutil.moveTo(f, 0, 0);
		}
	},
	
	_insertBefore: function(ul, selected, base) {
		for (var i = 0, f; i < selected.length; i++) {
			var f = selected[i];
			ul.insertBefore(f, base);
		}
	}
	
};
Yutil.en.register(Ydragdrop.SORTABLE_UL_SELECTOR, Ydragdrop.enhanceSortableUl);
